package com.jiang.books.service.jpa;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
//import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;

import org.apache.log4j.Logger;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.Lists;
import com.jiang.books.common.utils.annotation.AdditionalLogging;
import com.jiang.books.domain.Author;
import com.jiang.books.domain.AuthorSearchCriteria;
import com.jiang.books.domain.metamodel.Author_;
import com.jiang.books.repository.AuthorRepository;
import com.jiang.books.service.AuthorService;

@Service("jpaAuthorService")
@Repository
@Transactional
public class AuthorServiceImpl implements AuthorService {

	private static final Logger log = Logger.getRootLogger();
	
	@PersistenceContext
	private EntityManager entityManager;
	
	@Autowired
	private AuthorRepository repository;
	
	@Autowired
	private Validator validator;
	
	/*
	public AuthorServiceImpl() {
	}
	
	public AuthorServiceImpl(EntityManager entityManager) {
		log.debug("[AuthorServiceImpl][JPA] AuthorServiceImpl create instance");
		this.em = entityManager;
	}
	*/
	
	@Override
	@AdditionalLogging
	@Transactional(readOnly=true)
	public List<Author> findAll() {
		log.info("Author#Find All Authors");
		/*
		List<Author> authors = entityManager.createNamedQuery("Author.findAll", Author.class).getResultList();
		return authors;
		*/
		return Lists.newArrayList(repository.findAll());
	}

	@Override
    @Transactional(readOnly=true)
    public Author findById(BigInteger id) {
    	log.info("Author#Find Author by id: " + id);
    	/*
        TypedQuery<Author> query = entityManager.createNamedQuery("Author.findById", Author.class);
        query.setParameter("id", id);
        return query.getSingleResult();
        */
        return repository.findOne(id);
    }

    @Override
    @Transactional(readOnly=true)
    public Author findByIdAndRevision(BigInteger id, int revision) {
    	log.info("Author#Find Author by id: " + id + " and Revision: " + revision);
        AuditReader auditReader = AuditReaderFactory.get(entityManager);
        return auditReader.find(Author.class, id, revision);
    }
    
    @Override
	@Transactional(readOnly=true)
	public List<Author> findAuditById(BigInteger id) {
		log.info("Author#Find Author Audit by id: " + id);
		AuditReader auditReader = AuditReaderFactory.get(entityManager);
		List<Number> revisions = auditReader.getRevisions(Author.class, id);
		
		revisions = Lists.reverse(revisions);

		List<Author> authors = new ArrayList<Author>();
		Author author;
		for (Number revision: revisions) {
			author = auditReader.find(Author.class, id, revision);
			authors.add(author);
		}
		return authors;
	}

	@Override
	public Author save(Author author) {
		/*
		if (author.getId() == null) { // Insert author
			log.info("Author#Inserting new Author");
			entityManager.persist(author);
		} else {                      // Update author
			entityManager.merge(author);
			log.info("Author#Updating existing Author");
		}
		log.info("Author#Author saved with id: " + author.getId());
		return author;
		*/
		log.info("Author#Save Author with id: " + (!author.isNew() ? author.getId() : "not yet generated"));
		return repository.save(author);
	}

	@Override
	public void delete(Author author) {
		/*
		Author mergedAuthor = entityManager.merge(author);
		entityManager.remove(mergedAuthor);
        */
		repository.delete(author);
		log.info("Author#Author with id: " + author.getId() + " deleted successfully");
	}


    // CrudRepository
	@Override
    public List<Author> findByFirstName(String firstName) {
    	log.info("Author#Find Author by firstName: " + firstName);
        return repository.findByFirstName(firstName);
    }

	@Override
    public List<Author> findByFirstNameAndLastName(String firstName, String lastName) {
    	log.info("Author#Find Author by firstName: " + firstName + " and lastName: " + lastName);
        return repository.findByFirstNameAndLastName(firstName, lastName);
    }
	
	@Override
	@Transactional(readOnly=true)
	public Page<Author> findAllByPage(Pageable pageable) {
		return repository.findAll(pageable);
	}

    
    @Override
    @Transactional(readOnly=true)
    public List<Author> findByNativeQuery(String query) {
    	log.info("Author#Find Author by native query: " + query);
        return entityManager.createNativeQuery(query, "authorResult").getResultList();
    }

    @Override
	@Transactional(readOnly=true)
	public List<Author> findByCriteriaQuery(String firstName, String lastName) {
		log.info("Author#Find Author using CriteriaQuery by firstName: " + firstName + " and lastName: " + lastName);
		
		CriteriaBuilder cb = entityManager.getCriteriaBuilder();
		CriteriaQuery<Author> criteriaQuery = cb.createQuery(Author.class);
		Root<Author> authortRoot = criteriaQuery.from(Author.class);
		authortRoot.fetch(Author_.awards, JoinType.LEFT);
		authortRoot.fetch(Author_.books, JoinType.LEFT);

		criteriaQuery.select(authortRoot).distinct(true);
		
		Predicate criteria = cb.conjunction();
		
		// First Name
		if (firstName != null) {
			Predicate p = cb.equal(authortRoot.get(Author_.firstName), firstName);
			criteria = cb.and(criteria, p);
		}
		
		// Last Name
		if (lastName != null) {
			Predicate p = cb.equal(authortRoot.get(Author_.lastName), lastName);
			criteria = cb.and(criteria, p);
		}		
		
		criteriaQuery.where(criteria);
		List<Author> result = entityManager.createQuery(criteriaQuery).getResultList();
		return result;
	}
	
	@Override
	public Page<Author> findByCriteria(AuthorSearchCriteria searchCriteria, Pageable pageable) {
		// TODO Auto-generated method stub
		return null;
	}
	
	public Set<ConstraintViolation<Author>> validateAuthor(Author author) {
		return validator.validate(author);
	}
	
	
	//test
    @Override
    @Transactional(readOnly=true)
    public List<Author> findAllWithBooks() {
        List<Author> authors = entityManager.createNamedQuery("Author.findAllWithBooks", Author.class).getResultList();
        return authors;
    }

    @Transactional(readOnly=true)
    public List<Author> findAllWithDetail() {
        List<Author> contacts = entityManager.createNamedQuery("Author.findAllWithDetail", Author.class).getResultList();
        return contacts;
    }
}